/**
 * Copyright (C) 2009 Marcel Urbanek
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.urbiworx.sugar;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javax.faces.context.FacesContext;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.SAXException;

import com.google.inject.Binder;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Scope;
import com.google.inject.name.Named;
import com.google.inject.name.Names;

import de.urbiworx.sugar.annotations.ApplicationScope;
import de.urbiworx.sugar.annotations.RequestScope;
import de.urbiworx.sugar.annotations.SessionScope;


public class SugarConfigurationHolder {

	private static SugarConfigurationHolder instance=new SugarConfigurationHolder();
	private static boolean instanceInitialized=false;
	
	Logger logger=Logger.getLogger("de.urbiworx.sugar");
	
	private Injector injector;
	public Injector getInjector() {
		return injector;
	}


	public Map<String, SugarConfigurationEntry> getConfiguration() {
		return configuration;
	}
	
	public Map<String, SugarConfigurationEntry> getConfigurationByView() {
		return configurationByView;
	}
	
	public Map<String, SugarConfigurationEntry> getConfigurationByClass() {
		return configurationByClass;
	}
	private Map<String,SugarConfigurationEntry> configuration;
	private Map<String,SugarConfigurationEntry> configurationByView;
	private Map<String,SugarConfigurationEntry> configurationByClass;
	
	public static synchronized SugarConfigurationHolder getInstance()
	{
		if (!instanceInitialized)
		{
			instance.init(null,null);
		}
		return instance;
	}
	
	public static synchronized SugarConfigurationHolder init(URL configurationFile)
	{
		if (!instanceInitialized)
		{
			instance.init(null,configurationFile);
		}
		return instance;
	}
	
	/**
	 * Only call for test purposes, not in an productional environment 
	 * @param testConfiguration a map that contains Classes and Instances. If
	 * a Class (Key) in this map should be injected the given instance (Value)
	 * will be used 
	 */
	public static void testInit(final Map testConfiguration, URL configurationFile)
	{
		instance.init(testConfiguration,configurationFile);
	}
	
	/**
	 * Only call directly for test purposes, not in an productional environment 
	 * 
	 */
	public static void testReset()
	{
		instanceInitialized=false;
		instance=null;
	}
	
	private void init(final Map testConfiguration,URL configurationFile) {
		instanceInitialized=true;
		SAXParser parser;
		try {
			parser=SAXParserFactory.newInstance().newSAXParser();
		} catch (ParserConfigurationException e) {
			logger.severe("Following exception occured while trying to create SAXParser "+e.getMessage());
			return;
		} catch (SAXException e) {
			logger.severe("Following exception occured while trying to create SAXParser "+e.getMessage());
			return;
		}
		configuration=new HashMap<String,SugarConfigurationEntry>();
		configurationByView=new HashMap<String,SugarConfigurationEntry>();
		configurationByClass=new HashMap<String,SugarConfigurationEntry>();
		final List<SugarConfigurationEntry> entries=new LinkedList<SugarConfigurationEntry>();
		final List<AdditionalModuleEntry> additionalModules=new LinkedList<AdditionalModuleEntry>();	
		URL currentConfiguration=null;
		
		int readConfig=0;			
		Enumeration<URL> resources=null;
		try {
			resources = getClass().getClassLoader().getResources("META-INF/sugar-config.xml");
		} catch (IOException e1) {
			logger.warning("Error while trying to load sugar config from META-INF "+e1.getMessage());
		}
		while (resources!=null&&resources.hasMoreElements())
		{
			currentConfiguration=resources.nextElement();
			SugarContentHandler config=new SugarContentHandler(entries,additionalModules,configuration,configurationByView,configurationByClass,currentConfiguration.getFile().toString());
			try {
				parser.parse(currentConfiguration.openStream(), config );
			} catch (SAXException e) {
				logger.severe("Following exception occured while trying to parse configuration "+currentConfiguration.getFile()+" "+e.getMessage());
				return;
			} catch (IOException e) {
				logger.severe("Following exception occured while trying to parse configuration "+currentConfiguration.getFile()+" "+e.getMessage());
				return;
			}
			readConfig++;
			logger.info("loading sugar configuration:"+currentConfiguration.getFile());
		}
		currentConfiguration=null;
		try {
			if (configurationFile==null)
			{
				currentConfiguration=FacesContext.getCurrentInstance().getExternalContext().getResource("/WEB-INF/sugar-config.xml");
			}
			else
			{
				currentConfiguration=configurationFile;
			}
		} catch (Exception e1) {
			logger.warning("Error while trying to load sugar config from WEB-INF "+e1.getMessage());
		}
		if (currentConfiguration!=null)
		{
			SugarContentHandler config=new SugarContentHandler(entries,additionalModules,configuration,configurationByView,configurationByClass,currentConfiguration.getFile().toString());				
			try {
				parser.parse(currentConfiguration.openStream(), config );
			} catch (SAXException e) {
				logger.severe("Following exception occured while trying to parse configuration "+currentConfiguration.getFile()+" "+e.getMessage());
				return;
			} catch (IOException e) {
				logger.severe("Following exception occured while trying to parse configuration "+currentConfiguration.getFile()+" "+e.getMessage());
				return;
			}
			readConfig++;
			logger.info("loading sugar configuration:"+currentConfiguration.getFile());
		}
		if (readConfig==0)
		{
			logger.warning("No sugar-config.xml file found! Make sure your configuration(s) is/are in the META-INF or WEB-INF directory");				
		}
	
		
		injector = Guice.createInjector(new Module()
		{

			public void configure(Binder binder) {
				binder.bindScope(RequestScope.class, new JSFRequestScope());
				binder.bindScope(SessionScope.class, new JSFSessionScope());
				binder.bindScope(ApplicationScope.class, new JSFApplicationScope());				
				for (SugarConfigurationEntry entry:entries)
				{
					Class bindClazz;
					try {
						bindClazz = getClass().getClassLoader().loadClass(entry.getType());
					} catch (ClassNotFoundException e) {
						String msg="Class: "+entry.getType()+" defined in the sugar configuration not found (line: "+entry.getLine()+", file: "+entry.getFilename()+")";
						logger.severe(msg);
						throw new SugarException(msg);						
					}
					Class toClazz=null;
					if (entry.getImpl()!=null)
					{
						try {
							toClazz = getClass().getClassLoader().loadClass(entry.getImpl());
						} catch (ClassNotFoundException e) {
							String msg="Class: "+entry.getImpl()+" defined in the sugar configuration not found (line "+entry.getLine()+", file: "+entry.getFilename()+")";
							logger.severe(msg);
							throw new SugarException(msg);	
						}
					}
					if (entry.getImplInstance()!=null)
					{
						try {
							toClazz = getClass().getClassLoader().loadClass(entry.getImplInstance().substring(0, entry.getImplInstance().lastIndexOf('.')));
							Method staticMethod = toClazz.getMethod(entry.getImplInstance().substring(entry.getImplInstance().lastIndexOf('.')+1));
							
							Object o = staticMethod.invoke(toClazz.newInstance());
							binder.bind(bindClazz).toInstance(o);
							continue;
						} catch (ClassNotFoundException e) {
							String msg="Class: "+entry.getImpl()+" defined in the sugar configuration not found (line "+entry.getLine()+", file: "+entry.getFilename()+")";
							logger.severe(msg);
							throw new SugarException(msg);	
						} catch (SecurityException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (NoSuchMethodException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (IllegalArgumentException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (IllegalAccessException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (InvocationTargetException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (InstantiationException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
					
					Key bindKey=null;
					if (entry.getName()!=null&&entry.getName().equals("*"))
					{
						bindKey=Key.get(bindClazz, Named.class);
					}
					else if (entry.getName()!=null)
					{
						bindKey=Key.get(bindClazz, Names.named(entry.getName()));						
					}
					Scope scope=null;
					if (entry.getScope()!=SugarScopeHelper.NONE_SCOPE)
					{
						switch(entry.getScope())
						{
							case SugarScopeHelper.REQUEST_SCOPE:
								if (entry.isMultiverse())
								{
									scope=new JSFRequestScopeMultiverse();
								}
								else
								{
									scope=new JSFRequestScope();
								}
								break;
							case SugarScopeHelper.SESSION_SCOPE:
								if (entry.isMultiverse())
								{
									scope=new JSFSessionScopeMultiverse();
								}
								else
								{
									scope=new JSFSessionScope();
								}
								break;
							case SugarScopeHelper.APPLICATION_SCOPE:
								if (entry.isMultiverse())
								{
									scope=new JSFApplicationScopeMultiverse();
								}
								else
								{
									scope=new JSFApplicationScope();
								}
								break;
							case SugarScopeHelper.TWILIGHT_SCOPE:
								scope=new JSFTwilightScope();
								break;						
						}
					}	
					if (bindKey!=null&&toClazz!=null&&scope!=null)
					{
						if (testConfiguration!=null && testConfiguration.containsKey(bindKey))
						{
							binder.bind(bindKey).toInstance(testConfiguration.get(bindKey));
						} else {
							binder.bind(bindKey).to(toClazz).in(scope);	
						}
					}
					else if (bindKey!=null&&scope!=null)
					{
						if (bindClazz.isInterface())
						{
							String msg="Type:"+entry.getType()+" defined in the sugar configuration is an interface, expected is class because an to attribute is missing(line "+entry.getLine()+", file: "+entry.getFilename()+")";
							logger.severe(msg);
							throw new SugarException(msg);	
						}
						binder.bind(bindKey).to(bindClazz).in(scope);
					}
					else if (bindKey!=null)
					{
						binder.bind(bindKey).to(toClazz);
					}
					else if (scope!=null)
					{
						if(toClazz!=null)
						{
							if (testConfiguration!=null && testConfiguration.containsKey(bindClazz))
							{
								binder.bind(bindClazz).toInstance(testConfiguration.get(bindClazz));
							} else {
								binder.bind(bindClazz).to(toClazz);
								binder.bind(toClazz).in(scope);
							}
						}
						else
						{
							binder.bind(bindClazz).in(scope);
						}
					}
					else if ((bindClazz!=null)&&(toClazz!=null))
					{
						if (testConfiguration!=null)
						{
							if (testConfiguration.containsKey(bindClazz))
							{
								binder.bind(bindClazz).toInstance(testConfiguration.get(bindClazz));								
							}
							else
							{
								binder.bind(bindClazz).to(toClazz);
							}
						}
						else if (toClazz!=null)
						{
							binder.bind(bindClazz).to(toClazz);
						}
						else
						{
							binder.bind(bindClazz);
						}
					}
				}
				for (AdditionalModuleEntry entry:additionalModules)
				{
					entry.getModule().configure(binder);
				};	
			}
			
		});
	}

}
